# 객체 프로퍼티
# 프로퍼티의 종류
- 데이터 프로퍼티 (data property)
- 접근자 프로퍼티 (accessor property)
- 본질은 함수
- 값을 획득(get) / 설정(set)
- 외부 코드에서는 함수가 아닌 일반적인 프로퍼티처럼 보인다.
둘 중 한 종류에만 속할 수 있다.
Object.defineProperties({}, 'prop', {
get() { // 접근자 프로퍼티의 설명자
return 1;
},
value: 2 // 데이터 프로퍼티의 설명자
}) // error
2
3
4
5
6
# 프로퍼티 특성 설명자 Property Descriptor
ES5 이후, descriptor 값의 조회/수정이 가능해졌다.
# 프로퍼티 값과 플래그
객체 프로퍼티: 값(value) + 플래그(flag) 속성 세 가지
value
프로퍼티 값 정의- type: 자바스크립트에서 허용한 모든 값
- Default:
undefined
writable
true
값을 수정할 수 있다.
enumerable
true
반복문을 사용해 나열 가능
configurable
true
프로퍼티 삭제/플래그 수정 가능
프로퍼티 플래그의 기본값은 모두 true
# Object.getOwnPropertyDescriptor(obj, propertyName)
- 특정 프로퍼티에 대한 정보를 모두 반환
obj
정보를 얻고자 하는 객체propertyName
정보를 얻고자 하는 객체 내 프로퍼티
let user = {
name: "John"
};
let descriptor = Object.getOwnPropertyDescriptor(user, 'name');
alert( JSON.stringify(descriptor, null, 2 ) );
/* property descriptor:
{
"value": "John",
"writable": true,
"enumerable": true,
"configurable": true
}
*/
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Object.defineProperty(obj, propertyName, descriptor)
프로퍼티 플래그 변경
[
obj
,propertyName
] = [설명자를 적용하고 싶은 객체, 객체 프로퍼티]propertyName
이 없으면, 새로운 프로퍼티 생성
[
descriptor
] = [적용하고자 하는 프로퍼티 설명자]플래그 정보가 없으면 default 는
false
let user = {};
Object.defineProperty(user, "name", {
value: "John"
});
let descriptor = Object.getOwnPropertyDescriptor(user, 'name');
alert( JSON.stringify(descriptor, null, 2 ) );
/*
{
"value": "John",
"writable": false,
"enumerable": false,
"configurable": false
}
*/
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# writable 플래그
let user = {
name: "John"
};
Object.defineProperty(user, "name", {
writable: false
});
user.name = "Pete"; // Error: Cannot assign to read only property 'name'
2
3
4
5
6
7
8
9
비엄격 모드
- 읽기 전용 프로퍼티 값 수정 -> 에러 X
- BUT, 값 변경 X
- 플래그에서 정한 규칙을 위반 -> 에러 없이 그냥 무시
let user = { };
Object.defineProperty(user, "name", {
value: "John",
// defineProperty를 사용해 새로운 프로퍼티를 만들 땐, 어떤 플래그를 true로 할지 명시해주어야 합니다.
enumerable: true,
configurable: true
});
alert(user.name); // John
user.name = "Pete"; // Error
2
3
4
5
6
7
8
9
10
11
# enumerable 플래그
객체 내장 메서드 toString
- non-enumerable
- for...in, Object.keys(), Object.entries(), Object.values() 에서 순회하지 않음.
- custom toString 을 추가하면 for..in 에서 나타남
let user = {
name: "John",
toString() {
return this.name;
}
};
//커스텀 toString은 for...in을 사용해 열거할 수 있습니다.
for (let key in user) alert(key); // name, toString
Object.defineProperty(user, "toString", {
enumerable: false
});
// 이제 for...in을 사용해 toString을 열거할 수 없게 되었습니다.
for (let key in user) alert(key); // name
// 열거가 불가능한 프로퍼티는 Object.keys에도 배제됩니다.
alert(Object.keys(user)); // name
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# configurable 플래그
false
- 해당 프로퍼티는 제거/변경 X
- configurable/enumerable 플래그 수정 X
- writable
- false -> true (X)
- true -> false (O)
- 접근자 프로퍼티 get/set 을 변경 X (새롭게 만드는 것은 가능)
# Object.defineProperties(obj, descriptors)
프로퍼티 여러개를 한 번에 정의할 수 있다.
Object.defineProperties(user, {
name: { value: "John", writable: false },
surname: { value: "Smith", writable: false },
// ...
});
2
3
4
5
# Object.getOwnPropertyDescriptors
프로퍼티 설명자를 전부 한꺼번에 가져올 수 있다.
Object.defineProperties
와 함께 사용하면- 플래그, 심볼형 프로퍼티도 함께 복사
let clone = Object.defineProperties({}, Object.getOwnPropertyDescriptors(obj));
# 객체 수정을 막아주는 다양한 메서드
Object.preventExtensions(obj)
객체에 새로운 프로퍼티를 추가할 수 없게 합니다.Object.seal(obj)
새로운 프로퍼티 추가나 기존 프로퍼티 삭제를 막아줍니다. 프로퍼티 전체에 configurable: false를 설정하는 것과 동일한 효과입니다.Object.freeze(obj)
새로운 프로퍼티 추가나 기존 프로퍼티 삭제, 수정을 막아줍니다. 프로퍼티 전체에 configurable: false, writable: false를 설정하는 것과 동일한 효과입니다.
제약 사항을 확인하는 메서드
Object.isExtensible(obj)
새로운 프로퍼티를 추가하는 게 불가능한 경우 false를, 그렇지 않은 경우 true를 반환합니다.Object.isSealed(obj)
프로퍼티 추가, 삭제가 불가능하고 모든 프로퍼티가 configurable: false이면 true를 반환합니다.Object.isFrozen(obj)
프로퍼티 추가, 삭제, 변경이 불가능하고 모든 프로퍼티가 configurable: false, writable: false이면 true를 반환합니다.
# 접근자 프로퍼티 설명자 Accessor Descriptor
- value, writable 은 무시된다.
get
인수가 없는 함수, 프로퍼티를 읽을 때 동작set
인수가 하나인 함수, 프로퍼티에 값을 쓸 때 호출됨enumerable
configurable
let obj = {
get propName() {
// getter, obj.propName 을 실행할 때 실행된다.
},
set propName(value) {
// setter, obj.propName = value 를 실행할 때 실행되는 코드
}
}
2
3
4
5
6
7
8
바깥 코드에선 접근자 프로퍼티를 일반 프로퍼티처럼 사용할 수 있다.
let user = {
name: "John",
surname: "Smith",
get fullName() {
return `${this.name} ${this.surname}`;
}
};
alert(user.fullName);
2
3
4
5
6
7
8
프로퍼티에 getter 메서드만 있기 때문에 에러가 발생한다.
user.fullName = 'Test'; // Error
setter 추가
let user = {
// ...
set fullName(value) {
[this.name, this.surname] = value.split(" ");
}
};
user.fullName = "Alice Cooper";
console.log(user.name, user.surname); // Alice Cooper
2
3
4
5
6
7
8
fullName 은 가상의 프로퍼티 이다. 읽고 쓸 순 있지만 실존하지 않는다.
# 자바 getters, setters 의 용도
- 유효성 검사
- Lazy Loading
- Read 와 Write 권한을 다르게 설정
# 유효성 검사
클래스 외부에서 클래스의 private 필드 를 get/set 할때 사용한다.
- 클래스 필드를 실수로 조작하지 못하도록
유효성 검사
를 수행 할 수있는 중심 위치.
# 캡슐화
# 동시성과 멀티스레딩 -> 불변객체 생성
- 객체의 상태의 불변성을 위해 setter 가 없다.
- 객체를 동시에 실행중인 다른 스레드로 전달하려면, race condition 및 기타 멀티스레딩의 부작용을 피하기 위해 스레드들을 동기화 해야 한다.
- 불변 객체의 경우 객체 상태가 스레드에 의해 변경될까봐 걱정할 필요가 없다.
- 불변 객체 예제
public class MyObject {
// 모든 멤버 변수는 private - 캡슐화, final
private final int state;
private final String str;
// constructor(생성자) 는 변수를 정의 할 수있는 마지막 장소이다.
public MyObject(int state, String str) {
this.state = state;
this.str = str;
}
//Getters
public int getState() {
return this.state;
}
public String getStr() {
return this.str;
}
// 객체의 상태의 불변성을 위해 setter 가 없다.
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# getter 획득자 메서드
보통, 계산된 값을 반환
# Syntax
- 함수 이름
- 자바스크립트 변수 이름 생성 규칙 (_$0-9, A-Z,a-z) 에 허용 되는 문자열.
- 매개변수가 없다
# 선언
var log = ['test'];
var obj = {
get latest () {
if (log.length == 0) return undefined;
return log[log.length - 1]
}
}
2
3
4
5
6
7
프로퍼티 접근을 해도, 정의된 함수가 호출되고 값이 반환된다.
console.log (obj.latest); // "test"를 반환.
# 삭제
delete obj.latest
# defineProperty, 객체에 새 프로퍼티로 추가
const obj = { _name: 'DoonDoony' };
Object.defineProperty(obj, 'name', {
get: function() {
return `${this._name} the Cat!`;
},
enumerable: true,
configurable: true,
});
console.log(obj.name); // "DoonDoony the Cat!"
2
3
4
5
6
7
8
9
# 계산된 프로퍼티 이름
const prefix = 'name';
const obj = {
get [prefix] () {
return 'Getter is called with prefix'
}
}
2
3
4
5
6
# Immutable 패턴
객체의 생명주기 동안 내부의 상태가 절대 변경되지 않도록 강제하는 방법
- 객체의 프로퍼티 값 할당을 생성자를 통해서만 할 수 있다.
- 필드에 접근하기 위해서는 Getter 메서드를 사용해야 한다.
const _list = new WeakMap();
const key = new Object();
class SomeClass {
constructor(list) {
_list.set(key, list);
}
// 필드 자체 값 대신에 복사본을 반환
getList() {
// 객체를 복제하는 clone()이라는 메서드가 있다고 가정
return _list.get(key).clone();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# lazy evaluation
getter 프로퍼티에 접근하기 전까지는 값을 계산하지 않는다.
- 값의 계산 비용이 큰 경우
- RAM 이나 CPU 의 시간을 많이 소모할 때
- worker thread 생성 (?)
- 원격 파일 로드
- 값이 당장 필요하지 않을때, 나중에 이용될 때, 절대로 이용되지 않을 때
- 값이 다시 계산되어서는 안되는 경우.
- 값이 여러차례 이용되지만, 절대 변경되지 않아 매번 다시 계산할 필요가 없을 때
각 케이스가 궁금하다 TODO
# getter 반환값의 캐싱
getter 은 첫 호출 이후에는 다시 계산하지 않고 이 캐시 값을 반환한다.
setter 에서 delete 를 하든 getter 에서 delete 를 하든, delete 를 호출하면 setter, getter 모두가 해제 된다.
const o = {
_foo: '',
set foo (val) {
delete this.foo;
this.foo = val;
},
get foo () {
delete this.foo;
return this.foo = 'something';
}
};
o.foo = "test";
console.log(o.foo); // 'test' 출력
2
3
4
5
6
7
8
9
10
11
12
13
14
o.foo = "test"
실행 -> getter 인 foo 삭제console.log(o.foo)
실행get foo() {}
를 실행 Xo.foo
프로퍼티의 값을 반환하여 "test" 를 출력 O
🔗 o.foo = "test"
이후의 setter 가 활성화 되고, delete this.foo
는 setter 를 삭제하여
다음줄의 this.foo
는 객체의 프로퍼티에 값을 할당하게 됩니다.
그러나 만약에 delete this.foo
가 없다면, this.foo
는 다시 setter 를 호출하게 되므로 throws RangeError maximum call
를 유발합니다. 이것의 대안은 this._foo
처럼 프로퍼티 명을 private 형식으로 지정해 줄 수 있습니다.
var o = {
set foo (val) {
this._foo = val;
},
// ...
};
2
3
4
5
6
get foo() { delete this.foo }
foo 프로퍼티를 삭제해도 결과는 test
을 출력한다. 왜 delete 를 적어준 것일까요?
- 다시 o.foo 를 얻어 올때, getter 으로 값을 다시 계산하는 것을 없애기 위해서 입니다. 단순히 o의 프로퍼티인 foo 로서 값을 얻어 옵니다.
var o = {
_foo: '',
set foo (val) {
this._foo = val;
},
get foo () {
return this._foo = 'something';
}
};
2
3
4
5
6
7
8
9
return this._foo = 'something';
문자열을 반환하는 이유- 자바스크립트에서 할당문은 할당한 값을 반환 (const, let, var 키워드 제외)
이 예제는 getter 와 setter 를 캐싱으로서 사용하지 않을 때 입니다.
- 자바스크립트에서 할당문은 할당한 값을 반환 (const, let, var 키워드 제외)
o.foo = "test"; //test
o.foo; // something
2
아무리 setter 를 해서 _foo 의 값을 변경 시켜도, o.foo
으로 getter 를 호출해도 항상 'something' 으로 됩니다. this._foo = 'something'
에서 다시 setter 를 호출하기 때문입니다.
이 예제는 도대체 왜 만든건지 모르겠네요.
중요한 것은, 캐싱을 위해 의도적으로 getter 와 setter 를 사용할 수 있다는 점 같습니다.
# setter 설정자 메서드
const cat = {
_name: undefined,
set name(newName) {
this._name = newName;
},
call() {
console.log(this._name);
},
};
cat.name = 'DoonDoony';
cat.call(); // 'DoonDoony'
2
3
4
5
6
7
8
9
10
11
12
# setter 에서 값을 할당하는 내부적 과정
# 1. 접근하는 프로퍼티 탐색
직속 프로퍼티
접근하는 프로퍼티가 Accessor Descriptor(Getter/Setter) 일 때
- [Setter] 호출.
- [Setter] 가 없고 [Getter]만 있을 때
- 값 할당은 무시된다.
접근자 설명자가 아니고,
writable: false
일 때- 조용히 실패
- 엄격모드일 땐, Type Error 발생
1번 2번 모두 해당하지 않을때, 프로퍼티에 값을 세팅
접근하는 프로퍼티가 없을 때
- 객체와 연결된 상위 [[Prototype]] 체인을 순회한다.
# 2. 모든 [[Prototype]] 체인에서 프로퍼티가 발견되지 않을 경우
- Object extensible
- 직속 프로퍼티(Directly Present) 생성하고 값을 할당
- not Object extensible
- Object.preventExtensions(), Object.isExtensible()[false] 일 때,childCat 을 확장 불가능하게 만듭니다
'use strict'; const parentCat = { name: 'DoonDoon', age: 10, }; const childCat = Object.create(parentCat); Object.assign(childCat, { name: 'DoonDoony', age: 3, favorite: 'Red Ball' });
1
2
3
4
5
6
7
8
9Object.preventExtensions(childCat); // childCat 이 확장 불가능한지 확인합니다 console.log(Object.isExtensible(childCat)); // false
1
2
3 TypeError
발생childCat.gender = 'male'; // TypeError!
1- 프로퍼티의 삭제/수정 가능
childCat.favorite = 'Box'; console.log(childCat.favorite); // Box
1
2delete childCat.favorite; // true console.log(childCat.favorite); // undefined
1
2 - 오브젝트의 proto 에 값 추가, 변경 가능
[[Prototype]] 에 새 프로퍼티를 추가합니다
childCat.__proto__.gender = 'male'; console.log(Object.getPrototypeOf(childCat)); // { name: 'DoonDoon', age: 10, gender: 'male' }
1
2
- Object.preventExtensions(), Object.isExtensible()[false] 일 때,
# 3. 상위 [[Prototype]] 체인에서 프로퍼티가 발견된 경우 🔗
writable: true
- 직속 프로퍼티를 생성하고 값을 할당
- 의도는 상위 수준의 프로퍼티에 할당을 하려고 했지만 직속 프로퍼티가 추가 된다.
- 객체지향에서 Overriding 이라고 부르는 Shadowing 이 발생한다.
이것이 위임을 통한 프로퍼티 가려짐이다. anotherObj.a가 증가함이 아니라. myObject에 a가 새로 할당된다.let anotherObj = { a: 2, }; let myObject = Object.create(anotherObj); console.log(anotherObj.a); console.log(myObject.a); console.log(anotherObj.hasOwnProperty('a')); //ture console.log(myObject.hasOwnProperty('a')); // false
1
2
3
4
5
6
7
8
9
10
11console.log(myObject.a++); // 2; console.log(anotherObj.a); //2 ; console.log(myObject.a); // 3; console.log(myObject.hasOwnProperty('a')); // true
1
2
3
4
5
6- 직속 프로퍼티를 생성하고 값을 할당
writable: false
- 아무일도 일어나지 않는다.
- 엄격모드일 경우 TypeError
프로퍼티가 [Setter] 일 경우, 항상 Setter 가 호출된다.
- 직속 프로퍼티가 추가 되지 않는다.
- Setter 를 덮어 쓰려면
Object.defineProperty
를 사용해야 한다.
# getter 와 setter 똑똑하게 활용하기
# 1. 실제 프로퍼티 값을 감싸는 wrapper 처럼 사용
프로퍼티 값을 원하는 대로 통제 가능
let user = {
get name () {
return this._name;
},
set name (value) {
if (value.length < 4) {
alert('입력값이 너무 짧습니다. 4자 이상으로 입력하세요.');
return;
}
this._name = value;
}
};
user.name = "Pete";
alert(user.name);
user.name = "";
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
user._name 으로 접근할 수 있지만. _ 밑줄로 시작하는 프로퍼티는 객체 내부에서만 활용하고, 외부에서는 건드리지 않는 것이 관습.
# 2. 호환성을 위해 사용하기
데이터 프로퍼티 name, age 를 사용해서 사용자를 나타내는 객체 구현
function User(name, age) {
this.name = name;
this.age = age;
}
let john = new User("John", 25);
alert(john.age); // 25
2
3
4
5
6
요구사항이 바뀌어 age 를 birthday 로 저장해야 할 경우. (birthday 가 더 정확하고 편리하기 때문)
function User(name, birthday) {
this.name = name;
this.birthday = birthday;
}
let john = new User("John", new Date(1992, 6, 1));
2
3
4
5
기존 코드에 age 를 사용하고 있는 코드를 모두 찾아서 수정해야 하는 문제점이 있다.
function User(name, birthday) {
this.name = name;
this.birthday = birthday;
Object.defineProperties(this, 'age', {
get() {
let todayYear = new Date().getFullYear();
return todayYear - this.birthday.getFullYear();
}
});
}
let john = new User('John', new Date(1992, 6, 1));
alert(john.birthday);
alert(john.age);
2
3
4
5
6
7
8
9
10
11
12
13
# Property - Existence
객체에 프로퍼티가 존재하는지 여부를 확인하는 방법
const parent = { parentProp: 'Hey' };
const child = Object.create(parent);
child.childProp = 'Yay!';
2
3
in
연산자- [[Prototype]] 체인을 모두 순회하며 키 존재 여부를 검사한다.
console.log('parentProp' in child) // true
1Object.hasOwnProperty
- 직속 프로퍼티 존재 여부만을 검사한다.
console.log(child.hasOwnProperty('parentProp')); // false console.log(child.hasOwnProperty('childProp')); // true
1
2
# Object.seal
- 더 이상 확장 불가능 하게 만듦(프로퍼티 추가가 불가능한 상태)
'use strict';
const obj = { a: 1, b: 2 };
Object.seal(obj);
try {
obj.c = 3;
} catch (e) {
// 확장 불가능 하기 때문에, TypeError!
console.error(e); // TypeError: Cannot add property c, object is not extensible
}
2
3
4
5
6
7
8
9
10
- Object.defineProperty 로 Descriptor 를 변경하는 행위를 막음
try {
Object.defineProperty(obj, 'b', { enumerable: false });
} catch (e) {
console.error(e); // TypeError: Cannot redefine property: b
}
2
3
4
5
- 프로퍼티 삭제 불가
try {
delete obj.a;
} catch (e) {
console.error(e); // TypeError: Cannot delete property 'a' of #<Object>
}
2
3
4
5
# Object.freeze
- Object.seal 의 모든 동작을 포함
- 프로퍼티 값 또한 변경 불가능 하게 만든다.
- 중첩된 접근 값이 변경은 가능하다.
obj.a = { b: 1 }
이라면obj.a
는 변경 불가 하지만,obj.a.b
는 변경 가능합니다.